package idraw;

import geometry.*;

import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.Timer;

import idraw.MyKeyAdapter;
import idraw.MyTimer;

/**
 * Copyright 2007, 2008 Viera K. Proulx
 * This program is distributed under the terms of the 
 * GNU Lesser General Public License (LGPL)
 */


/**
 * World for programming interactive games - with graphics, key events,
 * mouse events and a timer. Designed to implement the same interface as
 * DrScheme/ProfessorJ world teachpack library.
 * 
 * @author Viera K. Proulx
 * @since November 15, 2007
 */
abstract public class World{

  /** the canvas that displays the current world */
  public Canvas theCanvas;

  /** true if 'bigBang' started the world and it did not end, did not stop */
  private boolean worldExists = false;

  /** the timer for this world */
  protected MyTimer mytime;

  /** the key adapter for this world*/
  private MyKeyAdapter ka;
 
  /**
   * The default constructor. To start the world one must invoke the
   * <code>bigBang</code> method.
   */
  public World(){ }

  /////////////////////////////////////////////////////////////////////////
  // Methods for interacting with the World                              //
  /////////////////////////////////////////////////////////////////////////

  /**
   * Start the world by creating a canvas of the given size, creating
   * and adding the key and mouse adapters, and starting the timer at the
   * given speed.
   * 
   * @param w the width of the <code>{@link Canvas Canvas}</code>
   * @param h the height of the <code>{@link Canvas Canvas}</code>
   * @param speed the speed at which the clock runs
   */
  public void bigBang(int w, int h, double speed){
    // throw runtime exceptions if w, h <= 0
    this.theCanvas = new Canvas(w, h);
    this.worldExists = true;

    // add the key listener to the frame for our canvas
    this.ka = new MyKeyAdapter(this);
    this.theCanvas.f.addKeyListener(ka);

    // make sure the canvas responds to events
    this.theCanvas.f.setFocusable(true);

    // add the timer -- start it if speed is greater than 0
    mytime = new MyTimer(this, speed);
    if (speed > 0.0)
      mytime.timer.start();

    // finally, show the canvas and draw the inital world
    this.theCanvas.show();
    this.drawWorld();
  }


  /**
   * Stop the timer, disable event processing - leave the canvas open, 
   * write the end of time message onto the canvas
   */
  public void endOfTime(String s){
    // remove listeners and set worldExists to false
    mytime.timer.stop();
    this.worldExists = false;
    this.theCanvas.f.removeKeyListener(this.ka);
    System.out.println("The world stopped: " + s);

    // draw the final scene of the world with the end of time message
    this.theCanvas.clear();
    this.draw();
    this.theCanvas.drawString(new Posn(10, 20),s);
  }


  /**
   * End the world interactions - leave the canvas open, 
   * write the end of world message onto the canvas
   */
  public void endOfWorld(String s){
    // remove listeners and set worldExists to false
    mytime.timer.stop();
    this.worldExists = false;
    this.theCanvas.f.removeKeyListener(this.ka);
    System.out.println("The end of the world: " + s);

    // draw the final scene of the world with the end of world message
    this.theCanvas.clear();
    this.draw();
    this.theCanvas.drawString(new Posn(10, 20),s);
  }

  /**
   * The method invoked by the timer on each tick.
   * Delegates to the user to define a new state of the world,
   * then resets the canvas and event handlers for the new world
   * to those currently used.
   * 
   * @return <code>{@link World World}</code> after the tick event
   */
  protected void processTick(){
    try{
      if (worldExists){
        this.onTick();
        this.drawWorld();
      }
    } 
    catch(RuntimeException re){
      re.printStackTrace();
      endOfTime(re.getMessage());
      //throw re;
      Runtime.getRuntime().halt(1);
    }
  }  

  /**
   * <P>User defined method to be invoked by the timer on each tick.
   * Update the <code>{@link World World}</code>.</P>
   * <P>Override this method in the game world class</P>
   */
  abstract public void onTick();
    

  /**
   * The method invoked by the key adapter on selected key events.
   * Delegates to the user to define a new state of the world,
   * then resets the canvas and event handlers for the new world
   * to those currently used.
   * 
   * @return <code>{@link World World}</code> after the key event
   */
  protected void processKeyEvent(String ke){
    try{
      if (worldExists){
        this.onKeyEvent(ke);
        this.drawWorld();
      }
    }
    catch(RuntimeException re){
      re.printStackTrace();
      endOfTime(re.getMessage());
      //throw re;
      Runtime.getRuntime().halt(1);
    }
  }

  /**
   * <P>User defined method to be invoked by the key adapter 
   * on selected key events.
   * Update the <code>{@link World World}</code>.</P>
   * <P>Override this method in the game world class</P>
   */
  abstract public void onKeyEvent(String s);

  /**
   * Invoke the user defined <code>draw</code> method, if this 
   * <code>{@link World World}</code> has been initialized 
   * via <code>bigBang</code> and did not stop or end 
   * via <code>endOfTime</code> 
   * or <code>endOfWorld</code>
   * 
   * @return <code>true</code>
   */
  protected void drawWorld(){
    if (this.worldExists){
      this.theCanvas.clear();
      this.draw();
    }
  }

  /**
   * <P>User defined method to draw the <code>{@link World World}</code>.</P>
   * <P>Override this method in the game world class</P>
   */
  abstract public void draw();
}

/**
 * The action listener for the timer events.
 * 
 * @author Viera K. Proulx
 * @since August 2, 2007
 */
class MyTimer{

  /** the current <code>{@link World World}</code> 
   * that handles the timer events */
  protected World currentWorld;

  /** the <code>Timer</code> that generates the time events */
  protected Timer timer;
  
  public boolean running = true;

  /** the timer speed */
  protected int speed;

  /**
   * Create the initial timer for the given 
   * <code>{@link World World}</code> at the given <code>speed</code>.
   * 
   * @param currentWorld the given <code>{@link World World}</code>
   * @param speed the given <code>speed</code>
   */
  protected MyTimer(World currentWorld, double speed){
    this.currentWorld = currentWorld;
    this.timer = new Timer((new Double(speed * 1000)).intValue(), 
        this.timerTasks);
    this.speed = (new Double(speed * 1000)).intValue();
  }

  /**
   * The callback for the timer events
   */
  protected ActionListener timerTasks = new ActionListener() {
    public void actionPerformed(ActionEvent evt) {
      if (running)
        currentWorld.processTick();
    }
  };

  /**
   * A helper method to convert the <code>speed</code> given as 
   * a delay time into milliseconds
   */
  protected void setSpeed(){ 
    this.timer.setDelay(this.speed);
  }
}

/**
 * <p>The implementation of callbacks for the key events.</p>
 * <p>Report all regular key presses and the four arrow keys</p>
 * <p>Ignore other key pressed events, key released events, 
 * special keys, etc.</p>
 * 
 * @author Viera K. Proulx
 * @since August 2, 2007
 */
class MyKeyAdapter extends KeyAdapter {

  /** the current <code>{@link World World}</code> 
   * that handles the key events */
  protected World currentWorld;

  /** the <code>KeyAdapter</code> that handles the key events */
  protected MyKeyAdapter(World currentWorld){
    this.currentWorld = currentWorld;
  }

  /**
   * The callback for the key events
   */
  protected void keyEventCallback(String keyString){
    currentWorld.processKeyEvent(keyString); 
  }

  // --------------------------------------------------------------------//
  // the key event handlers                                              //
  // --------------------------------------------------------------------//-

  /** 
   * <p>Handle the key typed event from the canvas.</p>
   * <p>This is where we get the letter keys.</p>
   *
   * @param e the <code>KeyEvent</code> that caused the callback
   */
  public void keyTyped(KeyEvent e) {
    displayInfo(e, "KEY TYPED: ");
  }

  /** 
   * <p>Handle the key pressed event from the canvas.</p>
   * <p>This is where we get the arrow keys.</p>
   *
   * @param e the <code>KeyEvent</code> that caused the callback
   */  
  public void keyPressed(KeyEvent e) {
    displayInfo(e, "KEY PRESSED: ");
  }

  /**
   * <p>The key event major processor. The code is adopted from the code
   * provided by the Java documentation for key event processing.</p>
   * 
   * <p><em>We have to jump through some hoops to avoid
   * trying to print non-printing characters 
   * such as Shift.  (Not only do they not print, 
   * but if you put them in a String, the characters
   * afterward won't show up in the text area.)</em></P>
   * 
   * <p>There may be unneeded code here. An earlier comment stated:
   * <em>needs to add conversion table from code to 
   * </em><code>String</code>. The code for converting a single character
   * to <code>String</code> is included here.</p>
   */
  protected void displayInfo(KeyEvent e, String s){
    String keyString ="";
    String modString, tmpString,
    actionString, locationString;

    //You should only rely on the key char if the event
    //is a key typed event.
    int id = e.getID();
    if (id == KeyEvent.KEY_TYPED) {
    	// regular letter has been pressed and released
    	char c = e.getKeyChar();
    	keyString = "" + c;

    	// process four arrow keys when pressed
    } else if (id == KeyEvent.KEY_PRESSED){
    	// check if pressed is one of the arrows
    	int keyCode = e.getKeyCode();
    	if (keyCode == 37)
    		keyString = "left"; 
    	else if (keyCode == 38)
    		keyString = "up"; 
    	else if (keyCode == 39)
    		keyString = "right"; 
    	else if (keyCode == 40)
    		keyString = "down"; 

    	else
    		; // ignore other keys pressed

      // record the KEY_RELEASED event
    } else {
      keyString = "released";
    }

    //////////////////////////////////////////////////////////////////////////
    // here is the actual callback                                          //
    if (!(keyString.length() == 0 ||    // ignore key pressed except arrows //
        keyString.equals("released")))  // ignore key released              //
      keyEventCallback(keyString);                                          //
    //////////////////////////////////////////////////////////////////////////
  } 
  // ---------------------------------------------------------------------
  // end of the key event major processor
}



